﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using ModelQuality.Metrics.Model;
using ModelQuality.Metrics.Configuration;
using ModelQuality.Metrics.Rules;
using log4net;
using System.Reflection;

namespace ModelQuality.Metrics.Engine
{
    public class ValidationEngine
    {
        private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

        private FileInfo xmlConfiguration;

        private ModelValidation modelValidation;

        private Activity selectedActivity;

        private IValidationContext validationContext;

        private event EventHandler ValidationRunEnded;

        public ValidationEngine(FileInfo configurationFile, IValidationContext validationContext)
        {
            this.xmlConfiguration = configurationFile;
            this.validationContext = validationContext;

            try
            {
                ConfigurationHelper helper = new ConfigurationHelper();
                // XML unmarshall
                modelValidation = helper.ParseXmlConfiguration(xmlConfiguration);
            }
            catch (Exception ex)
            {
                String message = String.Format("XML configuration file {0} is invalid: {1}", configurationFile.FullName, ex.Message);
                throw new EngineException(message, ex);
            }
        }

        public IList<Activity> GetActivities()
        {
            return modelValidation.activity.ToList().AsReadOnly();
        }

        public ValidationReport RunValidation(PackageVO startPackage, Activity activity)
        {
            selectedActivity = activity;

            // Reset from previous run
            ResetEngine();

            // Run
            ApplyRulesOnPackage(startPackage);

            if (ValidationRunEnded != null)
            {
                ValidationRunEnded(this, null);
            }

            return new ValidationReport(selectedActivity);
        }

        private void ResetEngine()
        {
            // Reset events
            ValidationRunEnded = null;

            // Reset rules
            selectedActivity.ResetRules();
        }

        private void ApplyRulesOnPackage(PackageVO startPackage)
        {
            // Recusion on sub-packages (depth-first)
            GetPackages(startPackage).ForEach(ApplyRulesOnPackage);

            // Apply rules on elements
            GetElements(startPackage).ForEach(ApplyRulesOnElement);

            // Apply rules on diagrams
            GetDiagrams(startPackage).ForEach(ApplyRulesOnDiagram);
        }

        private void ApplyRulesOnElement(ElementVO element)
        {
            // Recursion on sub-elements
            GetElements(element).ForEach(ApplyRulesOnElement);

            foreach (var rule in selectedActivity.elementRule)
            {
                ApplyRuleInternal(rule, element);
            }

            // Apply rules on associations
            GetAssociations(element).ForEach(ApplyRulesOnAssociation);
        }

        private void ApplyRulesOnDiagram(DiagramVO diagram)
        {
            foreach (var rule in selectedActivity.diagramRule)
            {
                ApplyRuleInternal(rule, diagram);
            }
        }

        private void ApplyRulesOnAssociation(AssociationVO association)
        {
            foreach (var rule in selectedActivity.associationRule)
            {
                ApplyRuleInternal(rule, association);
            }
        }

        private void ApplyRuleInternal<T>(IValidationRule<T> rule, T objectToValidate)
        {
            ValidationRule ruleBase = rule as ValidationRule;
            ruleBase.SetContext(validationContext);

            if (ruleBase.Active && rule.Matches(objectToValidate))
            {
                log.DebugFormat("Running {0} on object {1}", rule, objectToValidate);
                if (ruleBase.IsGlobal())
                {
                    // store information to be validated
                    ruleBase.ProcessGlobalRule(objectToValidate);
                    // validate stored information at the end of the run
                    ValidationRunEnded += (sender, e) =>
                        {
                            var res = ruleBase.EvaluateGlobalRule();
                            ruleBase.ProcessResult(res);
                        };
                }
                else
                {
                    var res = rule.Validate(objectToValidate);
                    ruleBase.ProcessResult(res);
                }
            }

            ruleBase.GetSubRules().Cast<IValidationRule<T>>().ToList()
                    .ForEach(subRule => ApplyRuleInternal(subRule, objectToValidate));
        }

        private List<PackageVO> GetPackages(PackageVO package)
        {
            return validationContext.RepositoryServices.GetPackages(package).ToList();
        }

        private List<ElementVO> GetElements(PackageVO package)
        {
            return validationContext.RepositoryServices.GetElements(package).ToList();
        }

        private List<ElementVO> GetElements(ElementVO element)
        {
            return validationContext.RepositoryServices.GetElements(element).ToList();
        }

        private List<DiagramVO> GetDiagrams(PackageVO package)
        {
            return validationContext.RepositoryServices.GetDiagrams(package).ToList();
        }

        private List<AssociationVO> GetAssociations(ElementVO element)
        {
            return validationContext.RepositoryServices.GetAssociations(element).ToList();
        }

    }
}
